/*
 * Copyright 2009 Jesse McLaughlin (nzjess@gmail.com)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.travelfusion.xmlclient.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import org.travelfusion.xmlclient.exception.TfXClientException;
import org.travelfusion.xmlclient.handler.Handler;

/**
 * A collection of helper methods aimed at those writing API {@link Handler} implementations.
 * <p>
 * The methods in this class are threadsafe.
 * 
 * @author Jesse McLaughlin (nzjess@gmail.com)
 * @author Dogan
 */
public class TfXAPIUtil {

  public static final long MILLIS_PER_SECOND = 1000L;
  public static final long MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60L;
  public static final long MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60L;
  public static final long MILLIS_PER_DAY = MILLIS_PER_HOUR * 24L;

  /**
   * The standard Date format used by the Travelfusion TripPlannerXML API service.
   */
  public static final String TRAVELFUSION_TRIPPLANNER_XML_DATE_FORMAT = "dd/MM/yyyy-HH:mm";

  /**
   * The standard public service endpoint URL for accessing the Travelfusion TripPlannerXML API service.
   */
  public static final String API_SERVICE_ENDPOINT_URL = "http://api.travelfusion.com/Xml";

  /**
   * Threadlocal {@link SimpleDateFormat} instance configured to parse dates in the standard TRAVELfusion TripPlannerXML
   * format, which is:
   * 
   * <pre>
   *   dd/MM/yyyy-HH:mm
   * </pre>
   * 
   * Where:
   * <ul>
   * <li><b>dd:</b> 2-digit day of the month (00-31)</li>
   * <li><b>MM:</b> 2-digit month of the year (01-12)</li>
   * <li><b>yyyy:</b> 4-digit year (eg. 2009)</li>
   * <li><b>HH:</b> 2-digit 24-hour of the day (00-23)</li>
   * <li><b>mm:</b> 2-digit minutes past the hour (00-59(</li>
   * </ul>
   */
  public static ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
    protected SimpleDateFormat initialValue() {
      SimpleDateFormat df = new SimpleDateFormat(TRAVELFUSION_TRIPPLANNER_XML_DATE_FORMAT);
      df.setTimeZone(TimeZone.getTimeZone("GMT"));
      return df;
    }
  };

  /**
   * Threadlocal {@link Calendar} instance.
   */
  public static ThreadLocal<Calendar> CALENDAR = new ThreadLocal<Calendar>() {
    protected Calendar initialValue() {
      return Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    }
  };

  /**
   * Parses a string into a date object using the standard date format.
   * 
   * @see #DATE_FORMAT
   * 
   * @throws TfXClientException If the string cannot be parsed.
   */
  public static Date parseDate(String string) {
    return parseDate(string, 0);
  }

  /**
   * Parses a string into a date object using the standard date format. Returns <code>null</code> if the passed in
   * string is <code>null</code> or contains only whitespace.
   * 
   * @see #DATE_FORMAT
   * 
   * @throws TfXClientException If the string cannot be parsed.
   */
  public static Date parseDateOrNull(String string) {
    return hasText(string) ? parseDate(string) : null;
  }

  /**
   * Parses a string into a date object using the standard date format, taking into account the given timezone
   * difference.
   * 
   * @param tzDeltaMinutes The number of minutes (plus or minus) between the timezone of the system where the response
   *          data will be used, and the timezone of the system invoking the TRAVELfusion TripPlanner XML service.
   * 
   * @see #DATE_FORMAT
   * 
   * @throws TfXClientException If the string cannot be parsed.
   */
  @Deprecated
  public static Date parseDate(String string, int tzDeltaMinutes) {
    try {
      return offsetTZ(DATE_FORMAT.get().parse(string), tzDeltaMinutes);
    }
    catch (ParseException e) {
      throw new TfXClientException(e);
    }
  }

  /**
   * Parses a string into a date object using the standard date format, taking into account the given timezone
   * difference. Returns <code>null</code> if the passed in string is <code>null</code> or contains only whitespace.
   * 
   * @see #DATE_FORMAT
   * 
   * @throws TfXClientException If the string cannot be parsed.
   */
  @Deprecated
  public static Date parseDateOrNull(String string, int tzDeltaMinutes) {
    return hasText(string) ? parseDate(string, tzDeltaMinutes) : null;
  }

  /**
   * Formats a date object as a string using the standard date format.
   * 
   * @see #DATE_FORMAT
   */
  public static String formatDate(Date date) {
    return DATE_FORMAT.get().format(date);
  }

  /**
   * Returns a date with a time component set to the closest midnight with respect to the date passed in, taking into
   * account the given timezone difference.
   */
  public static Date closestMidnight(Date date) {
    return closestMidnight(date, 0);
  }

  /**
   * Returns a date with a time component set to the closest midnight with respect to the date passed in, taking into
   * account the given timezone difference.
   */
  @Deprecated
  public static Date closestMidnight(Date date, int tzDeltaMinutes) {
    date = offsetTZ(date, -tzDeltaMinutes);
    date = new Date(date.getTime() + (MILLIS_PER_DAY / 2));
    return midnight(date, 0);
  }

  /**
   * Returns a date with a time component set to the midnight on the given date, plus or minus the given offset in days.
   */
  public static Date midnight(Date date, int offsetDays) {
    Calendar cal = CALENDAR.get();
    cal.setTime(date);
    cal.set(Calendar.HOUR_OF_DAY, 0);
    cal.set(Calendar.MINUTE, 0);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    cal.add(Calendar.DATE, offsetDays);
    return cal.getTime();
  }

  /**
   * Returns a date modified to take into account the given timezone difference.
   */
  @Deprecated
  public static Date offsetTZ(Date date, int tzDeltaMinutes) {
    if (tzDeltaMinutes == 0) return date;
    return new Date(date.getTime() - (tzDeltaMinutes * MILLIS_PER_MINUTE));
  }

  /**
   * Returns the difference between client and server timezone deltas (null-safe, where <code>null</code> means zero).
   */
  @Deprecated
  public static int combineTZDeltas(TimeZoneDelta clientTZDelta, TimeZoneDelta serverTZDelta) {
    int client = (clientTZDelta != null) ? clientTZDelta.getTZDeltaMinutes() : 0;
    int server = (serverTZDelta != null) ? serverTZDelta.getTZDeltaMinutes() : 0;
    return (client - server);
  }

  /**
   * Asserts that the passed in object is null. If it is not null, throws an IllegalStateException.
   * 
   * @param o The object to assert is null.
   * @param message The message to use to construct the exception that is thrown if the object is not null.
   */
  public static void isNull(Object o, String message) {
    if (o != null) throw new IllegalStateException(message);
  }

  /**
   * Asserts that the passed in object is not null. If it is null, throws an IllegalStateException.
   * 
   * @param o The object to assert is not null.
   * @param message The message to use to construct the exception that is thrown if the object is null.
   */
  public static <T> T notNull(T o, String message) {
    if (o == null) throw new IllegalStateException(message);
    return o;
  }

  /**
   * Tests if a string contains some text.
   * <p>
   * Formally, returns: <code>(string != null) && (string.trim().length() > 0)</code>.
   * 
   * @param string the string to test if it has text
   * @return true if the string is not null and has at least one non-whitespace character, false otherwise.
   */
  public static boolean hasText(String string) {
    return (string != null && string.trim().length() > 0);
  }
}
